/////////////////////////////////////////////////////////////////
// Script:  main script for cicp in second life
//          - rezzes cicp-prims
//          - rezzes cicp_gateway_select-prim to select gateway
//          - controls cicp_prims region-wide
//
/////////////////////////////////////////////////////////////////

/////////////////////////////////////////////////////////////////
// constants
/////////////////////////////////////////////////////////////////

// general
integer MAX_REZZER_SCRIPTS      = 3; // maximum of used rezzer scripts to throttle rez speed (see issue 1)

// configuration file
string  CONFIG_FILE             = "config"; // name of configurationfile
string  CONFIG_IGNORE_PREFIX    = "##"; // ignore lines starting with this (do not put "//" here, as it is problematic with URLs: "http://...")

// prim names
string  CICP_PRIM               = "cicp_prim"; // name of cicp prim in object's inventory
string  CICP_START_PRIM         = "cicp_start"; // name of starter prim in object's inventory

// menu button labels
string  LABEL_CONNECT_GATEWAY   = "Gateway"; // rez a cicp gateway prim
string  LABEL_REZ_SHEEPS        = "Rez prims"; // check for sheep prims and rez them
string  LABEL_ALL_RESET         = "Reset prims"; // reset all sheep prims
string  LABEL_ALL_DIE           = "Delete prims"; // delete all sheep prims
string  LABEL_RELOAD_CONFIG     = "Reload conf"; // reloads configuration

list    DEFAULT_BUTTONS         = [LABEL_REZ_SHEEPS, LABEL_ALL_RESET, // default menu ordering
                                   LABEL_ALL_DIE, LABEL_CONNECT_GATEWAY,
                                   LABEL_RELOAD_CONFIG];

// channels
integer CICP_PRIM_CHANNEL       = -30973762; // channel for all cicp prims (=atomic weight of Phosporus)
integer MENU_CHANNEL            = -30973763; // channel for llDialog communication
integer CICP_BACK_CHANNEL       = -30973770; // channel for responses to cicp init prim

// infoPushTypes (see info())
integer INFO_OUTPUT             = 1; // outputs message to owner
integer INFO_CLEAR              = 2; // clears earlier output, then adds message
integer INFO_NO_TEXT            = 4; // does not add infotext object's text with llSetText()
integer INFO_REMOVE_LAST_LINE   = 8; // removes last line (good for progress displays)
integer INFO_REMOVE_FIRST_LINE  = 16; // removes first line

// other infotext config
integer INFO_MAX_CHARS          = 255; // maximum displayed characters in infotext (please note the maximum of 255 chars by llSetText())
integer INFO_MAX_LINES          = 4; // maximum amount of lines displayed

// internal command prefixes to put commands in a specific context
integer CMD_ERROR               = 1; // an error occured
integer CMD_RESPONSE            = 2; // message is a response
integer CMD_REQUEST             = 4; // message is a request
// integer CMD_                 = 8; // reserved for probable later use

// internal commands
integer CMD_REZ                 = 16; // rezzing cicp_prims
integer CMD_CANCEL              = 32; // cancel current action
integer CMD_START_PARAM         = 64; // request/receive start param (see rezStartParam)
integer CMD_DETECT              = 128; // return script's name
integer CMD_COMPLETE            = 256; // operation has completed (response only)
integer CMD_PROGRESS            = 512; // message contains progress information

/////////////////////////////////////////////////////////////////
// global vars
/////////////////////////////////////////////////////////////////

string  infoText                = ""; // Current object's text set with llSetText()
vector  infoTextColor           = <0., 1., 0.>; // Color of infoText
float   infoTextAlpha           = 1.; // alpha transparency of infoText

string  objectTitle             = "Glasshouse Hub by Green Phosphor LLC"; // Title of object (overwritten by config cicp_hub_name)

list    config                  = [ // configuration, filled with default entries which will be
                                    // overwritten by the values in the confignotecard CONFIG_FILE
                                   "sheep_prims", 100,
                                   "cicp_url", "http://glasshouse.greenphosphor.com:8180/CICPManager?",
                                   "prim_channel_offset", 100,
                                   "ssl", FALSE,
                                   "cicp_hub_name", "Glasshouse Hub by Green Phosphor LLC"];
key     configQuery             = NULL_KEY; // Key of dataserverquery to get config file contents
integer configQueryLine         = 0; // line that is actually read from config file

integer totalSheepPrims         = 0; // set by configuration, total sheeps to rez
integer primChannelOffset       = 0; // set by configuration, channel offset of cicp prim channels
list    rezzerScripts           = []; // names of rezzer scripts in inventory
integer rezzedSheeps            = 0; // total rezzed sheeps
integer rezzerScriptsLeft       = 0; // rezzer scripts that have not yet finished their task

/////////////////////////////////////////////////////////////////
// functions
/////////////////////////////////////////////////////////////////

// transmits current configuration data to a cicp prim
// user receiver = CICP_PRIM_ID_ALL to publish it to all
publishConfig()
{
    llRegionSay(CICP_PRIM_CHANNEL, "config," + llList2CSV(config));
}

// Displays a menu to the user avatarId with the informational text infoMsg and the set
// of buttons defined with buttonList
showMenu(key avatarId, string infoMsg, list buttonList)
{
    llDialog(avatarId, infoMsg, buttonList, MENU_CHANNEL);
}

// initiates region at cicb hub
cicpInitRegion()
{
    llHTTPRequest(getConfig("cicp_url") + "type=initregion&region=" + llGetRegionName() + "&primcount=" + getConfig("sheep_prims"),
                  [HTTP_METHOD, getConfig("http_method"), HTTP_MIMETYPE, getConfig("http_mimetype"), HTTP_VERIFY_CERT, (integer)getConfig("http_use_ssl")],
                  "");
    info("Region initiated.");
}

// Displays msg as a new line with llSetText();
// infoPushType is a mask of flags (see INFO_-Constants) to adjust
// behaviour and overwrite default behaviour (infoPushType=0 for default behaviour)
info_(string msg, integer infoPushType)
{
    list lines = llParseString2List(infoText, ["\n"], []);

    // Process infoPushType-Flags
    if (infoPushType & INFO_OUTPUT) {
        llOwnerSay(msg);
    }
    if (infoPushType & INFO_CLEAR) {
        lines = [];
    }
    if (infoPushType & INFO_REMOVE_LAST_LINE) {
        // drop last line from infotext
        if (llGetListLength(lines) > 1) {
            lines = llList2List(lines, 0, -2);
        }
        else {
            lines = [];
        }
    }
    if (infoPushType & INFO_REMOVE_FIRST_LINE) {
        // drop first line from infotext
        if (llGetListLength(lines) > 1) {
            lines = llList2List(lines, 1, -1);
        }
        else {
            lines = [];
        }
    }
    if ((infoPushType & INFO_NO_TEXT) == FALSE) {
        lines += (list)msg;

        // check for max lines and cut first line if it exceeds limits
        if (llGetListLength(lines) > INFO_MAX_LINES) {
            lines = llList2List(lines, 1, -1);
        }
    }

    // remerge lines to text
    infoText = llDumpList2String(lines, "\n");
    // cut from the beginning if output gets bigger than INFO_MAX_CHARS chars
    integer maxMessageLength = INFO_MAX_CHARS - llStringLength(objectTitle);
    infoText = llGetSubString(infoText, -maxMessageLength, -1);

    string titleText = objectTitle;
    if (llStringLength(infoText) > 0) {
        // add horizontal line to separate title from infoText
        titleText += "\n----------------------------\n" + infoText;
    }

    llSetText(titleText, infoTextColor, infoTextAlpha);
}

// Quick function to display a message msg with default behaviour
info(string msg)
{
    info_(msg, 0);
}

// check config values for plausability and validity and returns TRUE if everything is ok
// returns FALSE if intereferences were detected and outputs an error message
integer checkConfig()
{
    list errorMessages = [];
    // validate number of sheep prims
    integer prims = (integer)getConfig("sheep_prims");
    if (prims <= 0) {
        errorMessages += ["sheep_prims must be greater than 0"];
    }
    // do channel checks
    integer lowestGraphChannel = (integer)getConfig("graph_channel_offset");
    integer highestGraphChannel = (integer)lowestGraphChannel + prims;
    integer lowestPrimChannel = (integer)getConfig("prim_channel_offset");
    integer highestPrimChannel = (integer)lowestPrimChannel + prims;
    // check graph channel range vs. prim channel range
    if (lowestGraphChannel <= highestPrimChannel) {
        integer difference = highestPrimChannel - lowestGraphChannel + 1;
        errorMessages += ["channels interfere: please set prim_channel_offset lower (" + (string)difference + ") or graph_channel_offset higher (" + (string)difference + ")"];
    }
    else if (highestGraphChannel >= -(CICP_PRIM_CHANNEL) && lowestGraphChannel-1 < -(CICP_PRIM_CHANNEL)) {
        integer difference = highestGraphChannel + CICP_PRIM_CHANNEL + 1;
        errorMessages += ["channels interfere: please set graph_channel_offset lower (" + (string)difference +")."];
    }
    // validate cicp hub url
    string url = llToLower(getConfig("cicp_url"));
    string protocol = llGetSubString(url, 0, 4);
    // check for http protocol
    if (protocol != "http:" && protocol != "https") {
        errorMessages += ["cicp_url specifies an unknown protocol (" + protocol + "). Please check if it starts with \"http\" or \"https\""];
    }
    // check for ssl
    if ((integer)getConfig("ssl") == TRUE) {
        if (protocol != "https") {
            errorMessages += ["ssl is set to true while cicp_url does not specify to use HTTPS. Please set cicp_url correctly to use SSL"];
        }
    }
    else if (protocol == "https") {
        // output a warning to user, that even the cicp_url is set to use httpS, http_use_ssl is set to false and ssl won't be used
        error("Your CICP URL specifies to use HTTPS, but http_use_ssl is set to false. SSL will not be used.");
    }    

    // check for query string separator "?"
    if (llGetSubString(url, -1, -1) != "?") {
        errorMessages += ["cicp_url needs a query string separator. Please add a question mark (\"?\") at the end of the URL."];
    }
    // return
    if (errorMessages == []) {
        return TRUE;
    }
    else {
        error("There were configuration conflicts detected. Please solve these conflicts first: " + llList2CSV(errorMessages));
        return FALSE;
    }
}

// reads a configuration variable and returns its value (see slcicp_init script)
string getConfig(string varName)
{
    integer varPos = llListFindList(llList2ListStrided(config, 0, -1, 2), (list)varName);
    if (~varPos) {
        varPos = (varPos << 1) + 1;
        return llList2String(config, varPos);
    }
    else {
        error("Config variable \"" + varName + "\" not found.");
        return "";
    }
}

// send internal message (mainly to slave rezzers)
sendMessage(integer cmd, string data, string receiver)
{
    llMessageLinked(LINK_THIS, cmd, data, (key)receiver);
}

// Throw error
error(string msg)
{
    msg = "Error: " + msg;
    llOwnerSay(msg);
    info(msg);
}

/////////////////////////////////////////////////////////////////
//  states: default, idle, countSheepPrims, rezSheeps, startRez_, loadConfig
/////////////////////////////////////////////////////////////////

 default
 {
     state_entry()
     {
         state loadConfig;
     }
 }

state idle
{
    state_entry()
    {
        // Listen for menu communication
        llListen(MENU_CHANNEL, "", NULL_KEY, "");
        llListen(CICP_BACK_CHANNEL, "", NULL_KEY, "");

        // set log clear timer
        llSetTimerEvent(3.);

        info("Ready.");
    }

    listen(integer c, string name, key id, string msg) {
        if (c == MENU_CHANNEL) {
            if (msg == LABEL_CONNECT_GATEWAY) {
                // rez a starter prim
                info("Rezzing CICP starter...");
                // check for existence of starter in object's inventory
                if (llGetInventoryType(CICP_START_PRIM) != INVENTORY_NONE) {
                    llRezObject(CICP_START_PRIM, llGetPos() + <0., 0., 1.>, ZERO_VECTOR, ZERO_ROTATION, 0);
                }
                else {
                    error("Could not rez cicp starter prim: \"" + CICP_START_PRIM + "\" is not in object's inventory.");
                }
            }
            else if (msg == LABEL_REZ_SHEEPS) {
                state rezSheeps;
            }
            else if (msg == LABEL_ALL_RESET) {
                llRegionSay(CICP_PRIM_CHANNEL, "reset");
                // re-init region
                cicpInitRegion();
            }
            else if (msg == LABEL_ALL_DIE) {
                llRegionSay(CICP_PRIM_CHANNEL, "die");
                // re-init region
                cicpInitRegion();

                // reset rezzed sheep variable
                rezzedSheeps = 0;
            }
            else if (msg == LABEL_RELOAD_CONFIG) {
                state loadConfig;
            }
        }
        else if (c == CICP_BACK_CHANNEL) {
            if (msg == "config") {
                // a config request, send config
                publishConfig();
            }
        }
    }

    touch_start(integer n)
    {
        // display default actions to user
        showMenu(llDetectedKey(0), "Please choose one of the following actions:", DEFAULT_BUTTONS);
    }

    timer()
    {
        // remove first line from infoText
        info_("", INFO_REMOVE_FIRST_LINE | INFO_NO_TEXT);
    }

    object_rez(key id)
    {
        // check if it's the gateway prim that has been rezzed
        if (llKey2Name(id) == CICP_START_PRIM) {
            info("Starter rezzed.");
        }
    }

    on_rez(integer i)
    {
        // reset script when rezzed
        llResetScript();
    }
}

state countSheepPrims
// counts existing sheep prims
// NOT USED YET (not stable)
{
    state_entry()
    {
        info("Counting sheep prims...");
        // reset rezzed sheep counter
        rezzedSheeps = 0;
        // listen to prim channel
        llListen(CICP_PRIM_CHANNEL, "", NULL_KEY, "");
        // shout a ping on prim channels and wait for them to answer
        // this is a "rough" check, where it is tried to cover as much as prims
        // possible without exceeding sl limits
        integer countSheepCheckGap = totalSheepPrims / 60;
        rezzedSheeps = rezzedSheeps + countSheepCheckGap;
        integer primChannel = -(totalSheepPrims - rezzedSheeps - countSheepCheckGap + primChannelOffset);

        llRegionSay(primChannel, "ping");
        // timeout for this action
        llSetTimerEvent(3.);
    }

    listen(integer channel, string name, key id, string message)
    {
        // directly check message as long as there are no other listens open in this state
        if (message == "pong") {
            integer countSheepCheckGap = totalSheepPrims / 60;
            rezzedSheeps = rezzedSheeps + countSheepCheckGap;
            integer primChannel = -(totalSheepPrims - rezzedSheeps - countSheepCheckGap + primChannelOffset);

            llRegionSay(primChannel, "ping");
        }
    }

    state_exit()
    {
        llSetTimerEvent(0.);
    }

    timer()
    {
        rezzedSheeps = rezzedSheeps + totalSheepPrims / 60;
        // print information if prims are missing
        if (rezzedSheeps < totalSheepPrims) {
            info("Approximately " + (string)(totalSheepPrims - rezzedSheeps) + " prims are missing. Click and choose \"Rez prims\" to rez missing prims.");
        }
        else {
            info("All prims rezzed.");
        }
        state idle;
    }
}

state rezSheeps
// rez sheeps by slave rezzers
{
    state_entry()
    {
        info("Rezzing sheep prims (" + (string)(totalSheepPrims - rezzedSheeps) + ")...Please wait.");
        // start detecting rezzer scripts
        rezzerScripts = [];
        sendMessage(CMD_REQUEST | CMD_DETECT, "", "");
        // set timeout for this action
        llSetTimerEvent(3.);
    }

    link_message(integer lN, integer cmd, string data, key id)
    {
        if (lN == llGetLinkNumber()) {
            if (cmd & CMD_RESPONSE) {
                if (cmd & CMD_DETECT) {
                    // add detected rezzer script to list
                    rezzerScripts += (list)data;
                }
            }
        }
    }

    state_exit()
    {
        llSetTimerEvent(0.);
    }

    timer()
    {
        // Start rezzing
        state startRez_;
    }
}

state startRez_
{
    state_entry()
    {
        // set progress display timer
        llSetTimerEvent(1.);
        // verify number of rezzers
        integer totalRezzer = llGetListLength(rezzerScripts);
        if (totalRezzer == 0) {
            error("No rezzer scripts found: Please put at least one rezzer script into slcicp prim.");
            state idle;
        }
        else if (totalRezzer > MAX_REZZER_SCRIPTS) {
            totalRezzer = MAX_REZZER_SCRIPTS;// limit number of used rezzerscripts (see issue 1)
            info("Using " + (string)MAX_REZZER_SCRIPTS + " rezzer scripts only. Please do not put more than " + (string)MAX_REZZER_SCRIPTS + " rezzer scripts into slcicp prim.");
        }
        // calculate prims per script (pps)
        integer totalPrims = totalSheepPrims - rezzedSheeps;
        integer pps = llFloor(totalPrims / totalRezzer);
        integer primRest = totalPrims % totalRezzer;
        list parameters = [CICP_PRIM, 0, pps]; // format is NAME_OF_CICP_PRIM, START_PARAM, PRIMS_TO_REZ
        integer i = 1; // start with 1 so we have a rezzer left to rez pps + primRest

        rezzerScriptsLeft = totalRezzer;

        // tell every rezzer script, how many prims it should rez
        for (; i < totalRezzer; i++) {
            // adjust start parameter and amount of prims
            parameters = llListReplaceList(parameters, [i*pps], 1, 1);
            sendMessage(CMD_REQUEST | CMD_REZ, llList2CSV(parameters), llList2String(rezzerScripts, i));

            // just a small delay for synchronizing rezzes
            llSleep(.1);
        }
        // let first rez script in list do rezzing of pps + rest prims
        parameters = llListReplaceList(parameters, [0, pps + primRest], 1, 2);
        sendMessage(CMD_REQUEST | CMD_REZ, llList2CSV(parameters), llList2String(rezzerScripts, 0));
    }

    timer()
    {
        string dot = "";
        if ((integer)llGetTime() % 2) {
            dot = " .";
        }
        else {
            dot = "  ";
        }

        // display rez progress
        info_("Rezzing prims " + (string)rezzedSheeps + "/"
              + (string)getConfig("sheep_prims") + dot,
              INFO_REMOVE_LAST_LINE);
    }

    state_exit()
    {
        // reset pending timers
        llSetTimerEvent(0.);
    }

    link_message(integer lN, integer cmd, string data, key id)
    {
        // receive rez success messages from rezzer scripts
        if (lN == llGetLinkNumber()) {
            if (cmd & CMD_RESPONSE) {
                if (cmd & CMD_REZ) {
                    if (cmd & CMD_PROGRESS) {
                        // sheep progress update
                        rezzedSheeps += (integer)data;
                    }
                    else if (cmd & CMD_COMPLETE) {
                        // rezzer script has completed
                        --rezzerScriptsLeft;
                        // check if all rezzers have finished
                        if (rezzerScriptsLeft == 0) {
                            // reset display timer
                            llSetTimerEvent(0.);
                            // add sheeps of last rezzer scripts
                            rezzedSheeps += (integer)data;
                            // check if amount of prims has finally been rezzed
                            if (rezzedSheeps < totalSheepPrims) {
                                error("Could not rez all needed prims ("
                                      + (string)rezzedSheeps + "/"
                                      + (string)totalSheepPrims + "). Please try again.");
                            }
                            else {
                                rezzedSheeps = totalSheepPrims;
                                info("All prims rezzed (" + (string)rezzedSheeps + ").");

                                // publish config to sheep prims
                                publishConfig();
                                // init region
                                cicpInitRegion();
                            }

                            // count sheep prims
                            // state countSheepPrims;
                            // move directly to idle state as long as countSheepPrims is not stable yet
                            state idle;
                        }
                    }
                }
            }
            else if (cmd & CMD_ERROR) {
                error("Could not rez sheep prims: " + data);
                state idle;
            }
        }
    }
}

state loadConfig
{
    state_entry()
    {
        info("Loading configuration...");
        // Check for config file existence
        if (llGetInventoryType(CONFIG_FILE) != INVENTORY_NONE) {
            // Start reading config notecard at line 0
            configQueryLine = 0;
            configQuery = llGetNotecardLine(CONFIG_FILE, configQueryLine);
        }
        else {
            info("No configuration (\"" + CONFIG_FILE + "\") found, continue using default config.");
            state idle;
        }
    }

    dataserver(key qId, string data)
    {
        if (configQuery == qId) {
            // parse configuration line from configuration file
            if (data != EOF) {
                // parse config line
                data = llStringTrim(data, STRING_TRIM_HEAD);
                // check if there is data
                if (llStringLength(data) > 0) {
                    // check if line is a comment
                    string prefix = llGetSubString(data, 0, llStringLength(CONFIG_IGNORE_PREFIX) - 1);
                    if (prefix != CONFIG_IGNORE_PREFIX) {
                        // it is not, read first value as variable name and second as its value
                        integer whiteSpacePos = llSubStringIndex(data, " ");
                        if (~whiteSpacePos) {
                            // read name from line beginning to first whitespace
                            string varName = llGetSubString(data, 0, whiteSpacePos - 1);

                            // read value from whitespace until CONFIG_IGNORE_PREFIX or line end
                            string varValue = llGetSubString(data, whiteSpacePos+1, -1);

                            // check for comments at the end of line
                            integer commentPos = llSubStringIndex(varValue, CONFIG_IGNORE_PREFIX);
                            if (~commentPos) {
                                // cut away
                                varValue = llGetSubString(varValue, 0, commentPos - 1);
                            }

                            // remove whitespaces around value
                            varValue = llStringTrim(varValue, STRING_TRIM);
                            // parse varValue if needed
                            if (llToLower(varValue) == "true") {
                                varValue = "1";
                            }
                            else if (llToLower(varValue) == "false") {
                                varValue = "0";
                            }

                            // check if varName exists in configset and overwrite its value
                            integer configPos = llListFindList(llList2ListStrided(config, 0, -1, 2),
                                                               (list)varName);
                            if (~configPos) {
                                // move to value entry
                                configPos = (configPos << 1) + 1;
                                // overwrite config value
                                config = llListReplaceList(config, (list)varValue, configPos, configPos);
                            }
                            else {
                                // add new configuration variable to config
                                config += [varName, varValue];
                            }
                        }
                    }
                }
                ++configQueryLine;
                configQuery = llGetNotecardLine(CONFIG_FILE, configQueryLine);
            }
            else {
                // validate config
                if (checkConfig()) {
                    // save quick access variables
                    totalSheepPrims = (integer)getConfig("sheep_prims");
                    primChannelOffset = (integer)getConfig("prim_channel_offset");
                    objectTitle = getConfig("cicp_hub_name");

                    // publish config to prims
                    publishConfig();
                }
                // change to idle state when finished
                state idle;
            }
        }
    }
}
